package com.andova.component.config;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;

import com.andova.component.AndovaComponent;
import com.andova.component.config.convert.JsonToXml;
import com.andova.component.config.convert.XmlToJson;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import static com.andova.component.AndovaComponentManager.TAG;
import static com.andova.component.config.convert.XmlToJson.DEFAULT_CONTENT_NAME;

/**
 * Created by Administrator on 2018-08-16.
 *
 * @author kzaxil
 * @since 0.0.2
 */
public class AndovaConfigComponent extends AndovaComponent {
    private final String SHARED_PREFERENCE_NODE_CONFIG_SUFFIX = "-alternative";
    private final String SHARED_PREFERENCE_NAME_CONFIG_STORER = "shared_preference_name_config_storer";

    private final String CONFIG_KEY_NAME_ID = "id";
    private final String CONFIG_KEY_NAME_URL = "url";
    private final String CONFIG_KEY_NAME_LOG = "log";
    private final String CONFIG_KEY_NAME_VALUE = "value";
    private final String CONFIG_KEY_NAME_CONFIG = "config";
    private final String CONFIG_KEY_NAME_VERSION = "version";
    private final String CONFIG_KEY_NAME_ALTERNATIVE = "alternative";
    private final String CONFIG_KEY_NAME_DEFAULT_JSON_CONTENT = DEFAULT_CONTENT_NAME;
    private final String CONFIG_FILE_NAME = "component-" + CONFIG_KEY_NAME_CONFIG + ".xml";

    private File mFile;
    @NonNull
    private Config mConfig;
    @Deprecated
    private SharedPreferences mSharedPrefCache;

    @Keep
    public AndovaConfigComponent(@NonNull Config config) {
        mConfig = config;
    }

    @Override
    protected void onCreate(@NonNull Application cxt) {
        List<ConfigBean> configs = getValidConfig();
        if (configs == null || configs.size() == 0) return;

        File dir = cxt.getExternalFilesDir(CONFIG_KEY_NAME_CONFIG);
        mFile = new File(dir, CONFIG_FILE_NAME);

        initSharedPreference(cxt, configs);

        FileInputStream fis = null;
        try {
            if (!mFile.exists() && !mFile.createNewFile()) return;
            fis = new FileInputStream(mFile);
            JSONObject json = convertToJson(cxt.getPackageName(), new XmlToJson.Builder(fis, null).build().toJson(), configs);
            JsonToXml.Builder builder = fixNode(json);
            fixOldNode(json, configs, builder);
            writeConfigFile(builder.build().toFormattedString());
        } catch (IOException | JSONException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Deprecated
    private void initSharedPreference(@NonNull Application cxt, List<ConfigBean> configs) {
        if (configs == null || configs.size() == 0) return;
        mSharedPrefCache = cxt.getSharedPreferences(SHARED_PREFERENCE_NAME_CONFIG_STORER, Context.MODE_PRIVATE);
        int version = mSharedPrefCache.getInt(CONFIG_KEY_NAME_VERSION, 0);
        Log.i(TAG, "current SharedPreference version is " + version + ", Config#version()=" + mConfig.version());
        boolean isNewVersion = version < mConfig.version();
        mSharedPrefCache.edit().putInt(CONFIG_KEY_NAME_VERSION, mConfig.version()).apply();
        removeRedundant();
        if (!isNewVersion) {
            Log.i(TAG, "this is not new version config, ignore update SharedPreference!");
            return;
        }
        writeToSharedPreference(configs);
    }

    @Deprecated
    private void removeRedundant() {
        if (mSharedPrefCache == null) return;
        if (mFile == null || !mFile.exists()) return;
        FileInputStream fis;
        try {
            fis = new FileInputStream(mFile);
            XmlToJson xmlToJson = new XmlToJson.Builder(fis, null).build();
            JSONObject json = xmlToJson.toJson();
            if (json == null) return;
            JSONObject config = json.getJSONObject(CONFIG_KEY_NAME_CONFIG);
            Set<String> keys = new HashSet<>();
            Iterator<String> iterator = config.keys();
            while (iterator.hasNext()) {
                keys.add(iterator.next());
            }
            Set<String> spKeys = mSharedPrefCache.getAll().keySet();
            for (String key : spKeys) {
                if (key.endsWith(SHARED_PREFERENCE_NODE_CONFIG_SUFFIX)) continue;
                if (keys.contains(key)) continue;
                mSharedPrefCache.edit().remove(key).remove(key + SHARED_PREFERENCE_NODE_CONFIG_SUFFIX).apply();
            }
        } catch (FileNotFoundException | JSONException e) {
            Log.e(TAG, "remove redundant is exception, rewrite config xml file.");
        }
    }

    @Deprecated
    private void writeToSharedPreference(List<ConfigBean> configs) {
        if (mSharedPrefCache == null) return;
        for (ConfigBean config : configs) {
            mSharedPrefCache.edit().putString(config.node, config.value).apply();
            if (config.alternative == null || config.alternative.size() == 0) continue;
            mSharedPrefCache.edit().putStringSet(config.node + SHARED_PREFERENCE_NODE_CONFIG_SUFFIX, config.values()).apply();
        }

        ValueBean urlBean = mConfig.url();
        if (urlBean == null) return;
        mSharedPrefCache.edit().putString(CONFIG_KEY_NAME_URL, urlBean.value).apply();
        if (urlBean.alternative == null || urlBean.alternative.size() == 0) return;
        mSharedPrefCache.edit().putStringSet(CONFIG_KEY_NAME_URL + SHARED_PREFERENCE_NODE_CONFIG_SUFFIX, urlBean.values()).apply();

        mSharedPrefCache.edit().putString(CONFIG_KEY_NAME_LOG, String.valueOf(mConfig.log())).apply();
        Set<String> alternative = new HashSet<>(2);
        alternative.add(String.valueOf(true));
        alternative.add(String.valueOf(false));
        mSharedPrefCache.edit().putStringSet(CONFIG_KEY_NAME_LOG + SHARED_PREFERENCE_NODE_CONFIG_SUFFIX, alternative).apply();
    }

    @Nullable
    private List<ConfigBean> getValidConfig() {
        ConfigBean[] beans = mConfig.node();
        if (beans == null) return null;
        List<ConfigBean> configs = new ArrayList<>(beans.length);
        for (ConfigBean config : beans) {
            if (config.node.endsWith(SHARED_PREFERENCE_NODE_CONFIG_SUFFIX)) {
                Log.e(TAG, "bad ConfigBean '" + config.node + "', it should not end with '-alternative', ignore this ConfigBean!");
                continue;
            }
            if (config.node.equals(CONFIG_KEY_NAME_CONFIG)) {
                Log.e(TAG, "bad ConfigBean 'config', you should not named 'config' node, ignore this ConfigBean!");
                continue;
            }
            if (config.node.equals(CONFIG_KEY_NAME_URL)) {
                Log.e(TAG, "bad ConfigBean 'url', you should not named 'url' node, ignore this ConfigBean!");
                continue;
            }
            configs.add(config);
        }
        return configs;
    }

    private JSONObject convertToJson(String applicationId, JSONObject json, List<ConfigBean> configs) throws JSONException {
        JSONObject object = null;
        if (json == null) {
            json = new JSONObject();
        }
        if (!json.has(CONFIG_KEY_NAME_CONFIG)) {
            object = new JSONObject();
            json.put(CONFIG_KEY_NAME_CONFIG, object);
        }
        if (object == null) object = json.getJSONObject(CONFIG_KEY_NAME_CONFIG);
        object.put(CONFIG_KEY_NAME_ID, applicationId);
        if (object.has(CONFIG_KEY_NAME_VERSION)) {
            try {
                int version = Integer.parseInt(object.getString(CONFIG_KEY_NAME_VERSION));
                if (version >= mConfig.version()) {
                    Log.i(TAG, "this is not new version config, ignore update xml file!");
                    return json;
                }
            } catch (NumberFormatException | NullPointerException e) {
                Log.e(TAG, "parse version error, " + e.getMessage());
            }
        }
        object.put(CONFIG_KEY_NAME_VERSION, mConfig.version());
        for (ConfigBean config : configs) {
            JSONObject value = configBeanToJsonObject(config);
            if (value != null) object.put(config.node, value);
        }

        JSONObject urlObject = configBeanToJsonObject(mConfig.url());
        if (urlObject != null) object.put(CONFIG_KEY_NAME_URL, urlObject);

        Set<Value> alternative = new HashSet<>(2);
        alternative.add(new Value(String.valueOf(true)));
        alternative.add(new Value(String.valueOf(false)));
        JSONObject logObject = configBeanToJsonObject(new ConfigBean(CONFIG_KEY_NAME_LOG, String.valueOf(mConfig.log()), alternative));
        if (logObject != null) object.put(CONFIG_KEY_NAME_LOG, logObject);

        json.put(CONFIG_KEY_NAME_CONFIG, object);
        return json;
    }

    @Nullable
    private JSONObject configBeanToJsonObject(ValueBean config) throws JSONException {
        if (config == null) return null;
        JSONObject value = new JSONObject();
        if (config.alternative != null && config.alternative.size() > 0) {
            JSONArray array = new JSONArray();
            for (Value v : config.alternative) {
                array.put(v.value);
            }
            value.put(CONFIG_KEY_NAME_ALTERNATIVE, array);
        }
        if (!TextUtils.isEmpty(config.value)) {
            value.put(CONFIG_KEY_NAME_VALUE, config.value);
        }
        return value;
    }

    private void writeConfigFile(String content) {
        if (mFile == null) return;
        FileWriter fw = null;
        BufferedWriter bw = null;
        try {
            if (!mFile.exists() && !mFile.createNewFile()) return;
            fw = new FileWriter(mFile);
            bw = new BufferedWriter(fw);
            bw.write(content);
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fw != null) fw.close();
                if (bw != null) bw.close();
            } catch (IOException ignore) {
            }
        }
    }

    private JsonToXml.Builder fixNode(@NonNull JSONObject json) {
        JsonToXml.Builder builder = new JsonToXml.Builder(json);
        builder.forceAttribute("/" + CONFIG_KEY_NAME_CONFIG + "/" + CONFIG_KEY_NAME_ID);
        builder.forceAttribute("/" + CONFIG_KEY_NAME_CONFIG + "/" + CONFIG_KEY_NAME_VERSION);
        builder.forceAttribute("/" + CONFIG_KEY_NAME_CONFIG + "/" + CONFIG_KEY_NAME_URL + "/" + CONFIG_KEY_NAME_VALUE);
        List<String> nodes = getConfigNode(json);
        if (nodes == null) return builder;
        for (String node : nodes) {
            builder.forceAttribute("/" + CONFIG_KEY_NAME_CONFIG + "/" + node + "/" + CONFIG_KEY_NAME_VALUE)
                    .forceContent("/" + CONFIG_KEY_NAME_CONFIG + "/" + node + "/" + CONFIG_KEY_NAME_ALTERNATIVE + "/" + CONFIG_KEY_NAME_DEFAULT_JSON_CONTENT);
        }
        return builder;
    }

    private void fixOldNode(JSONObject json, List<ConfigBean> configs, JsonToXml.Builder builder) {
        Iterator<String> keys;
        try {
            keys = json.getJSONObject(CONFIG_KEY_NAME_CONFIG).keys();
            Set<String> set = new HashSet<>(configs.size());
            for (ConfigBean config : configs) {
                set.add(config.node);
            }
            while (keys.hasNext()) {
                String key = keys.next();
                if (set.contains(key)) continue;
                builder.forceAttribute("/" + CONFIG_KEY_NAME_CONFIG + "/" + key + "/" + CONFIG_KEY_NAME_VALUE)
                        .forceContent("/" + CONFIG_KEY_NAME_CONFIG + "/" + key + "/" + CONFIG_KEY_NAME_ALTERNATIVE + "/" + CONFIG_KEY_NAME_DEFAULT_JSON_CONTENT);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onLowMemory(@NonNull Application cxt) {

    }

    @Nullable
    private List<String> getConfigNode(@NonNull JSONObject json) {
        try {
            json = json.getJSONObject(CONFIG_KEY_NAME_CONFIG);
            Iterator<String> keys = json.keys();
            List<String> nodes = new ArrayList<>(5);
            while (keys.hasNext()) {
                String key = keys.next();
                if (key.equals(CONFIG_KEY_NAME_ID) || key.equals(CONFIG_KEY_NAME_VERSION)) continue;
                nodes.add(key);
            }
            return nodes;
        } catch (JSONException e) {
            Log.e(TAG, "get config node list fail, " + e.getMessage());
            return null;
        }
    }

    @Keep
    @Nullable
    public List<String> getConfigNode() {
        if (!mConfig.enable()) {
            Log.e(TAG, "config disable, get node list fail!");
            return null;
        }
        JSONObject json = getConfigOfJson();
        Log.i(TAG, "getConfigNode# xml of config : \n" + (json == null ? null : json.toString()));
        if (json == null) return null;
        return getConfigNode(json);
    }

    @Keep
    @NonNull
    public String getValueOfNode(@NonNull String node) {
        JSONObject json = getConfigOfJson();
        Log.i(TAG, "getValueOfNode# xml of config : \n" + (json == null ? null : json.toString()));
        if (json == null) return "";
        try {
            String value = json.getJSONObject(CONFIG_KEY_NAME_CONFIG).getJSONObject(node).getString(CONFIG_KEY_NAME_VALUE);
            if (mConfig.enable()) {
                return TextUtils.isEmpty(value) ? "" : value;
            }
            ConfigBean[] beans = mConfig.node();
            if (beans == null) return "";
            for (ConfigBean bean : beans) {
                if (!node.equals(bean.node)) continue;
                return bean.value;
            }
            return "";
        } catch (JSONException | NullPointerException e) {
            Log.e(TAG, "get node '" + node + "' value fail, " + e.getMessage());
            return "";
        }
    }

    @Keep
    @Nullable
    public Set<String> getAlternativeOfNode(String node) {
        if (!mConfig.enable()) {
            Log.e(TAG, "config disable, get node '" + node + "' alternative fail!");
            return null;
        }
        JSONObject json = getConfigOfJson();
        Log.i(TAG, "getAlternativeOfNode# xml of config : \n" + (json == null ? null : json.toString()));
        if (json == null) return null;
        try {
            JSONArray array = json.getJSONObject(CONFIG_KEY_NAME_CONFIG).getJSONObject(node).getJSONArray(CONFIG_KEY_NAME_ALTERNATIVE);
            Set<String> nodes = new HashSet<>(array.length());
            for (int i = 0; i < array.length(); i++) {
                nodes.add(array.getJSONObject(i).getString(CONFIG_KEY_NAME_DEFAULT_JSON_CONTENT));
            }
            return nodes;
        } catch (JSONException | NullPointerException e) {
            Log.e(TAG, "get node '" + node + "' alternative fail, " + e.getMessage());
            return null;
        }
    }

    @Keep
    public void updateValueOfNode(String node, Value value) {
        JSONObject json = getConfigOfJson();
        if (json == null) return;
        try {
            JSONObject config = json.getJSONObject(CONFIG_KEY_NAME_CONFIG);
            JSONObject object = config.getJSONObject(node);
            object.put(CONFIG_KEY_NAME_VALUE, value.value);
            config.put(node, object);
            json.put(CONFIG_KEY_NAME_CONFIG, config);
            String xmlOfConfig = fixNode(json).build().toFormattedString();
            updateSharedPreferenceValueOfNode(node, value);
            updateValueOfNode(xmlOfConfig);
            Log.i(TAG, "updateValueOfNode# xml of config : \n" + xmlOfConfig);
        } catch (JSONException | NullPointerException | ClassCastException e) {
            Log.e(TAG, "update node '" + node + "' value to '" + value + "' fail, " + e.getMessage());
        }
    }

    private void updateValueOfNode(String xml) {
        if (mFile == null) return;
        FileInputStream fis = null;
        try {
            if (!mFile.exists() && !mFile.createNewFile()) return;
            fis = new FileInputStream(mFile);
            writeConfigFile(xml);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Deprecated
    private void updateSharedPreferenceValueOfNode(String node, Value value) {
        if (mSharedPrefCache == null) return;
        if (!mSharedPrefCache.contains(node)) return;
        mSharedPrefCache.edit().putString(node, value.value).apply();
    }

    @Nullable
    private JSONObject getConfigOfJson() {
        if (mFile == null) return null;
        FileInputStream fis = null;
        try {
            if (!mFile.exists()) return null;
            fis = new FileInputStream(mFile);
            return new XmlToJson.Builder(fis, null).build().toJson();
        } catch (IOException e) {
            Log.e(TAG, "get config text of json format fail, " + e.getMessage());
            return null;
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Keep
    @NonNull
    public String getValueOfUrl() {
        if (mConfig.enable()) {
            return getValueOfNode(CONFIG_KEY_NAME_URL);
        }
        ValueBean value = mConfig.url();
        return value == null || TextUtils.isEmpty(value.value) ? "" : value.value;
    }
}
